package com.harmontronics.hanma.paas.paas.domain.app.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.harmontronics.hanma.paas.paas.common.PageBase;
import com.harmontronics.hanma.paas.paas.consts.DbConstants;
import com.harmontronics.hanma.paas.paas.consts.NumberEnum;
import com.harmontronics.hanma.paas.paas.domain.app.entity.HmFolder;
import com.harmontronics.hanma.paas.paas.domain.app.entity.HmModel;
import com.harmontronics.hanma.paas.paas.domain.app.entity.HmPage;
import com.harmontronics.hanma.paas.paas.domain.app.entity.HmProcess;
import com.harmontronics.hanma.paas.paas.domain.app.entity.valueobject.BpmnConstant;
import com.harmontronics.hanma.paas.paas.domain.app.entity.valueobject.Fields;
import com.harmontronics.hanma.paas.paas.domain.app.entity.valueobject.FlowableConstant;
import com.harmontronics.hanma.paas.paas.domain.app.entity.valueobject.I18nKeyEnum;
import com.harmontronics.hanma.paas.paas.domain.app.repository.persistence.HmProcessRepository;
import com.harmontronics.hanma.paas.paas.domain.app.repository.po.HmProcessPO;
import com.harmontronics.hanma.paas.paas.exception.ErrorKeyException;
import com.harmontronics.hanma.paas.paas.utils.EscapeUtil;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.ExtensionElement;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.api.FlowableObjectNotFoundException;
import org.flowable.common.engine.impl.util.io.BytesStreamSource;
import org.flowable.common.engine.impl.util.io.InputStreamSource;
import org.flowable.common.engine.impl.util.io.StreamSource;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.DeploymentBuilder;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author ldb
 */
@Service("hmProcessService")
@Slf4j
@DS("#header.App-Unique")
public class HmProcessService {

    @Autowired
    private HmProcessRepository hmProcessRepository;

    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private HmModelService hmModelService;

    @Autowired
    private HmPageService hmPageService;
    @Autowired
    private HmFolderService hmFolderService;


    public HmProcess findById(Long id, Boolean includeXml) {
        HmProcessPO po = hmProcessRepository.getById(id);
        HmProcess hmProcess = BeanUtil.toBean(po, HmProcess.class);
        if (includeXml != null && includeXml && !StringUtils.isEmpty(po.getDeploymentId())) {
            try {
                String json = getProcessDefinitionJson(po.getDeploymentId(), po.getKey() + "_" + po.getVersion() + ".bpmn");
                hmProcess.setJson(json);
            } catch (Exception ex) {
                log.error(ex.getMessage(), ex);
            }
        }
        HmFolder folder = hmFolderService.findById(hmProcess.getFolderId());
        hmProcess.setFolderName(folder.getName());
        return hmProcess;
    }


    public List<HmProcess> getAndCheckPublish() {
        List<HmProcessPO> pos = hmProcessRepository.findUnPackedActivated();
        for (HmProcessPO po : pos) {
            if (po.getPublished() == null || po.getPublished() == NumberEnum.ZERO.getNum()) {
                throw new ErrorKeyException(I18nKeyEnum.PROCESS_VERSION_NOT_PUBLISHED, po.getKey(), po.getVersion().toString());
            }
        }
        return Convert.toList(HmProcess.class, pos);
    }

    public void makePacked(Long id) {
        HmProcessPO po = hmProcessRepository.getById(id);
        po.setPacked(NumberEnum.ONE.getNum());
        hmProcessRepository.updateById(po);
    }


    public void save(HmProcess hmProcess) {
        validWhenCreate(Fields.KEY, hmProcess.getKey());
        HmProcessPO po = BeanUtil.toBean(hmProcess, HmProcessPO.class);
        po.setVersion(NumberEnum.ONE.getNum());
        po.setActivated(NumberEnum.ONE.getNum());
        po.setPublished(NumberEnum.ZERO.getNum());
        po.setPacked(NumberEnum.ZERO.getNum());
        hmProcessRepository.save(po);
    }

    public void update(HmProcess hmProcess) {
        validWhenUpdate(hmProcess.getId(), Fields.KEY, hmProcess.getKey());
        HmProcessPO po = hmProcessRepository.getById(hmProcess.getId());
        po.setName(hmProcess.getName());
        po.setDescription(hmProcess.getDescription());
        hmProcessRepository.updateById(po);
    }

    public void remove(List<Long> ids) {
        for (Long id : ids) {
            HmProcessPO hmProcessPO = hmProcessRepository.getById(id);
            if (hmProcessPO == null) {
                continue;
            }
            hmProcessRepository.removeByKey(hmProcessPO.getKey());
        }
    }

    public List<HmProcess> getList(Boolean activated) {
        if (activated != null && activated) {
            List<HmProcessPO> pos = hmProcessRepository.findActivated();
            return Convert.toList(HmProcess.class, pos);
        } else {
            List<HmProcessPO> pos = hmProcessRepository.list();
            return Convert.toList(HmProcess.class, pos);
        }
    }

    public PageBase<HmProcess> getList(Long folderId, String searchKey, int pageNo, int pageSize) {
        Page<HmProcessPO> page = new Page<>(pageNo, pageSize);
        QueryWrapper<HmProcessPO> queryWrapper = new QueryWrapper<>();
        if (folderId != null) {
            queryWrapper.eq(Fields.FOLDER_ID, folderId);
        }
        if (!StringUtils.isEmpty(searchKey)) {
            String escapeStr = EscapeUtil.escapeChar(searchKey);
            queryWrapper.and(wrapper -> wrapper.like(Fields.KEY_DB, escapeStr).or().like(Fields.NAME, escapeStr));
        }
        queryWrapper.eq(Fields.ACTIVATED, NumberEnum.ONE.getNum());
        queryWrapper.orderByDesc(DbConstants.CREATE_DATE);
        IPage<HmProcessPO> resultPage =
                hmProcessRepository.selectPage(queryWrapper, page);
        return PageBase.from(resultPage, HmProcess.class);
    }

    public void validField(Long id, String key, String value) {
        if (id == null) {
            validWhenCreate(key, value);
        } else {
            validWhenUpdate(id, key, value);
        }

    }

    private void validWhenUpdate(Long id, String key, String value) {
        if (Fields.NAME.equals(key)) {
            List<HmProcessPO> pos = hmProcessRepository.findByName(value);
            if (pos.isEmpty()) {
                return;
            }
            HmProcessPO hmProcessPO = hmProcessRepository.getById(id);
            long count = pos.stream().filter(e -> !e.getKey().equals(hmProcessPO.getKey())).count();
            if (count > 0) {
                throw new ErrorKeyException(I18nKeyEnum.PROCESS_NAME_EXISTS);
            }
        }
        if (Fields.KEY.equals(key)) {
            List<HmProcessPO> pos = hmProcessRepository.findByKey(value);
            if (pos.isEmpty()) {
                return;
            }
            HmProcessPO hmProcessPO = hmProcessRepository.getById(id);
            long count = pos.stream().filter(e -> !e.getKey().equals(hmProcessPO.getKey())).count();
            if (count > 0) {
                throw new ErrorKeyException(I18nKeyEnum.PROCESS_KEY_EXISTS);
            }
        }
    }

    private void validWhenCreate(String key, String value) {
        if (Fields.NAME.equals(key)) {
            List<HmProcessPO> pos = hmProcessRepository.findByName(value);
            if (!pos.isEmpty()) {
                throw new ErrorKeyException(I18nKeyEnum.PROCESS_NAME_EXISTS);
            }
        }
        if (Fields.KEY.equals(key)) {
            List<HmProcessPO> pos = hmProcessRepository.findByKey(value);
            if (!pos.isEmpty()) {
                throw new ErrorKeyException(I18nKeyEnum.PROCESS_KEY_EXISTS);
            }
        }

    }

    /**
     * 流程移动目录
     *
     * @param id
     * @param folderId
     */
    public void move(Long id, Long folderId) {
        HmProcessPO po = hmProcessRepository.getById(id);
        required(po);
        List<HmProcessPO> pos = hmProcessRepository.findByKey(po.getKey());
        pos.forEach(e -> e.setFolderId(folderId));
        hmProcessRepository.updateBatchById(pos);
    }

    /**
     * 检查对象是否存在
     *
     * @param hmProcessPO
     */
    private void required(HmProcessPO hmProcessPO) {
        if (hmProcessPO == null) {
            throw new ErrorKeyException(I18nKeyEnum.PROCESS_NOT_FOUND);
        }
    }

    /**
     * 新建版本（可改描述）
     *
     * @param sourceId
     * @param description
     */
    public void makeVersion(Long sourceId, String sourceKey, String description) {

        List<HmProcessPO> pos = hmProcessRepository.findByKey(sourceKey);

        if (sourceId == null) {
            HmProcessPO demo = pos.get(0);
            HmProcessPO targetPo = new HmProcessPO();
            targetPo.setActivated(NumberEnum.ZERO.getNum());
            targetPo.setPublished(NumberEnum.ZERO.getNum());
            int maxVersion = pos.stream().mapToInt(HmProcessPO::getVersion).max().orElse(0);
            targetPo.setVersion(maxVersion + 1);
            targetPo.setDescription(description);
            targetPo.setName(demo.getName());
            targetPo.setKey(sourceKey);
            targetPo.setFolderId(demo.getFolderId());
            targetPo.setPacked(NumberEnum.ZERO.getNum());
            hmProcessRepository.save(targetPo);
        } else {
            HmProcessPO po = hmProcessRepository.getById(sourceId);
            required(po);
            int maxVersion = pos.stream().mapToInt(HmProcessPO::getVersion).max().orElse(po.getVersion());
            HmProcessPO targetPo = new HmProcessPO();
            BeanUtil.copyProperties(po, targetPo, "id", "createDate", "createBy", "modifyBy", "modifyDate");
            targetPo.setVersion(maxVersion + 1);
            targetPo.setActivated(NumberEnum.ZERO.getNum());
            targetPo.setPublished(NumberEnum.ZERO.getNum());
            targetPo.setDeploymentId(null);
            if (!StringUtils.isEmpty(description)) {
                targetPo.setDescription(description);
            }
            targetPo.setPacked(NumberEnum.ZERO.getNum());
            hmProcessRepository.save(targetPo);
            if (po.getDeploymentId() != null) {
                String json = getProcessDefinitionJson(BeanUtil.toBean(po, HmProcess.class));
                Deployment deployment = doDeployment(json, targetPo);
                targetPo.setDeploymentId(deployment.getId());
                hmProcessRepository.updateById(targetPo);
            }
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void activateVersion(Long id) {
        HmProcessPO po = hmProcessRepository.getById(id);
        required(po);
        if (po.getDeploymentId() == null) {
            throw new ErrorKeyException(I18nKeyEnum.MUST_DESIGN_PROCESS_FIRST);
        }
        //其他版本失活
        List<HmProcessPO> pos = hmProcessRepository.findByKey(po.getKey());
        for (HmProcessPO hmProcessPO : pos) {
            if (hmProcessPO.getActivated() == NumberEnum.ONE.getNum()) {
                hmProcessPO.setActivated(NumberEnum.ZERO.getNum());
                hmProcessRepository.updateById(hmProcessPO);
                if (!StringUtils.isEmpty(hmProcessPO.getDeploymentId())) {
                    suspendProcessDefinition(hmProcessPO.getDeploymentId());
                }
            }
        }
        po.setActivated(NumberEnum.ONE.getNum());
        hmProcessRepository.updateById(po);
        activateProcessDefinition(po.getDeploymentId());
    }

    /**
     * 激活流程
     *
     * @param deploymentId
     */
    private void activateProcessDefinition(String deploymentId) {
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deploymentId).singleResult();
        if (processDefinition.isSuspended()) {
            repositoryService.activateProcessDefinitionById(processDefinition.getId(), false, null);
        }
    }

    /**
     * 挂起流程
     *
     * @param deploymentId
     */
    private void suspendProcessDefinition(String deploymentId) {
        try {
            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deploymentId).singleResult();
            if (!processDefinition.isSuspended()) {
                repositoryService.suspendProcessDefinitionById(processDefinition.getId(), false, null);
            }
        } catch (Exception ex) {
            log.error(ex.getMessage(), ex);
        }
    }

    public List<HmProcess> getHistoryList(String key) {
        List<HmProcessPO> pos = hmProcessRepository.findByKey(key);
        return Convert.toList(HmProcess.class, pos);
    }

    /**
     * 部署流程设计
     *
     * @param id
     * @param json
     */
    @Transactional(rollbackFor = Exception.class)
    public void deploy(Long id, String json) {
        HmProcessPO po = hmProcessRepository.getById(id);
        required(po);

        //检查是否可以部署
        if (!ObjectUtils.isEmpty(po.getDeploymentId())) {
            long count = runtimeService.createProcessInstanceQuery().deploymentId(po.getDeploymentId()).count();
            if (count > 0) {
                throw new ErrorKeyException(I18nKeyEnum.PROCESS_DEFINITION_UNDER_USING);
            }
        }
        //一定要加后缀.bpmn 否是无法识别process definition
        Deployment deployment = doDeployment(json, po);

        //移除之前的部署
        if (po.getDeploymentId() != null) {
            repositoryService.deleteDeployment(po.getDeploymentId());
        }


        po.setDeploymentId(deployment.getId());
        hmProcessRepository.updateById(po);
    }

    private Deployment doDeployment(String json, HmProcessPO po) {
        String resourceName = po.getKey() + "_" + po.getVersion() + ".bpmn";
        DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();
        deploymentBuilder.addBytes(resourceName, json.getBytes(StandardCharsets.UTF_8));
        StreamSource xmlSource = new BytesStreamSource(json.getBytes(StandardCharsets.UTF_8));
        BpmnModel bpmnModel = new BpmnXMLConverter().convertToBpmnModel(xmlSource, false, false, "UTF-8");
        org.flowable.bpmn.model.Process process = bpmnModel.getMainProcess();
        Collection<FlowElement> flowElements = process.getFlowElements();
        Map<String, List<ExtensionElement>> extensionMap = process.getExtensionElements();
        bindModel(po, extensionMap);
        Map<String, String> formKeyMap = new HashMap<String, String>(16);
        for (FlowElement flowElement : flowElements) {
            String formKey = null;
            if (flowElement instanceof UserTask) {
                UserTask userTask = (UserTask) flowElement;
                if (userTask.getFormKey() != null && userTask.getFormKey().length() > 0) {
                    formKey = userTask.getFormKey();
                }
            }
            if (!StringUtils.isEmpty(formKey) && !formKeyMap.containsKey(formKey)) {
                String formKeyDefinition = formKey.replace(".form", "");
                HmPage form = hmPageService.findById(Long.parseLong(formKeyDefinition), true);
                if (!StringUtils.isEmpty(form.getJson())) {
                    byte[] formJson = form.getJson().getBytes(StandardCharsets.UTF_8);
                    ByteArrayInputStream bi = new ByteArrayInputStream(formJson);
                    deploymentBuilder.addInputStream(formKey, bi);
                    formKeyMap.put(formKey, formKey);
                } else {
                    throw new FlowableObjectNotFoundException("Cannot find formJson with formKey " + formKey);
                }
            }
        }
        deploymentBuilder.name(resourceName);
        Deployment deployment = deploymentBuilder.deploy();
        return deployment;
    }


    private void bindModel(HmProcessPO po, Map<String, List<ExtensionElement>> extensionMap) {
        List<ExtensionElement> list = extensionMap.get(BpmnConstant.FIELD);
        if (CollectionUtils.isEmpty(list)) {
            throw new ErrorKeyException(I18nKeyEnum.PROCESS_DEFINITION_MISSING_MODEL_BINDING);
        }
        ExtensionElement extensionElement = list.get(0);
        if (BpmnConstant.FIELD.equals(extensionElement.getName())) {
            String value = extensionElement.getAttributeValue(null, BpmnConstant.NAME);
            if (BpmnConstant.MODEL_FIELD.equals(value)) {
                Map<String, List<ExtensionElement>> childExtensionMap = extensionElement.getChildElements();
                List<ExtensionElement> elementList = childExtensionMap.get(BpmnConstant.STRING);
                if (!CollectionUtils.isEmpty(elementList)) {
                    ExtensionElement stringElement = elementList.get(0);
                    if (stringElement.getName().equals(BpmnConstant.STRING)) {
                        String modelIdStr = stringElement.getElementText();
                        Long modelId = Long.parseLong(modelIdStr);
                        HmModel hmModel = hmModelService.findById(modelId);
                        po.setModelCode(hmModel.getCode());
                        hmProcessRepository.updateById(po);
                        return;
                    }
                }
            }
        }
        throw new ErrorKeyException(I18nKeyEnum.PROCESS_DEFINITION_MISSING_MODEL_BINDING);
    }

    public List<HmProcess> findByDeploymentIdIn(List<String> deploymentIds) {
        if (CollectionUtils.isEmpty(deploymentIds)) {
            return new ArrayList<>();
        }
        List<HmProcessPO> pos = hmProcessRepository.findByDeploymentIdIn(deploymentIds);
        return Convert.toList(HmProcess.class, pos);
    }

    public String getProcessDefinitionJson(HmProcess hmProcess) {
        String resourceName = hmProcess.getKey() + "_" + hmProcess.getVersion() + ".bpmn";
        return getProcessDefinitionJson(hmProcess.getDeploymentId(), resourceName);
    }

    /**
     * 获取流程配置json(根据resourceName也可获取表单信息)
     *
     * @param deploymentId
     * @param resourceName
     * @return
     */
    public String getProcessDefinitionJson(String deploymentId, String resourceName) {
        InputStream inputStream = null;
        try {
            inputStream = repositoryService.getResourceAsStream(deploymentId, resourceName);
            int available = inputStream.available();
            byte[] data = new byte[available];
            inputStream.read(data);
            return new String(data);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new ErrorKeyException(I18nKeyEnum.PROCESS_DEFINITION_GET_ERROR);
        } finally {
            if (inputStream != null) {
                IoUtil.close(inputStream);
            }
        }
    }

    public void copy(Long sourceId, String name, String key, String description) {
        validWhenCreate(Fields.KEY, key);
        HmProcessPO sourcePo = hmProcessRepository.getById(sourceId);
        required(sourcePo);
        HmProcessPO po = new HmProcessPO();
        po.setName(name);
        po.setKey(key);
        po.setVersion(NumberEnum.ONE.getNum());
        po.setActivated(NumberEnum.ONE.getNum());
        po.setDescription(description);
        po.setFolderId(sourcePo.getFolderId());
        hmProcessRepository.save(po);
        //拷贝流程设计
        if (StringUtils.isEmpty(sourcePo.getDeploymentId())) {
            return;
        }
        //    repositoryService.
    }

    public Long getStartFormId(Long processId) {
        HmProcessPO po = hmProcessRepository.getById(processId);
        required(po);
        HmProcess hmProcess = BeanUtil.toBean(po, HmProcess.class);
        hmProcess.checkActivated();
        String resourceName = po.getKey() + "_" + po.getVersion() + ".bpmn";
        InputStream inputStream = repositoryService.getResourceAsStream(po.getDeploymentId(), resourceName);


        BpmnModel bpmnModel = new BpmnXMLConverter().convertToBpmnModel(new InputStreamSource(inputStream), false, false, "UTF-8");
        org.flowable.bpmn.model.Process process = bpmnModel.getMainProcess();
        Collection<FlowElement> flowElements = process.getFlowElements();
        for (FlowElement flowElement : flowElements) {
            if (flowElement instanceof UserTask) {
                UserTask userTask = (UserTask) flowElement;
                if (!userTask.getId().equals(FlowableConstant.INITIATOR)) {
                    continue;
                }
                String formIdStr = userTask.getFormKey();
                if (formIdStr == null) {
                    return null;
                }
                return Long.parseLong(formIdStr.replace(".form", ""));
            }
        }
        return null;
    }

    public List<HmProcess> getUsableProcess(Long modelId) {
        HmModel hmModel = hmModelService.findById(modelId);
        List<HmProcessPO> processPOS = hmProcessRepository.findUsable(hmModel.getCode());
        return Convert.toList(HmProcess.class, processPOS);
    }

    public ProcessDefinition getProcessDefinition(Long processId) {
        if (processId == null) {
            return null;
        }
        HmProcess hmProcess = findById(processId, false);
        return repositoryService.createProcessDefinitionQuery().deploymentId(hmProcess.getDeploymentId()).singleResult();
    }

    public void publish(Long id) {
        HmProcessPO po = hmProcessRepository.getById(id);
        if (po != null) {
            po.setPublished(NumberEnum.ONE.getNum());
            hmProcessRepository.updateById(po);
        }
    }


}